﻿#if UNITY_EDITOR
using UnityEngine;
using UnityEditor;

namespace AI.Graph.Editor {


    [CustomEditor (typeof (AIPath))]
    public class AIPathInspector : UnityEditor.Editor {

    #region Fields

        private AIPath m_path;
        private Vector3 m_oldTransformPosition;
        private SerializedProperty m_points;
        private SerializedProperty m_isPathClosed;
        private bool m_showPoints;

        private float m_scale = 1;

        private int m_selectedMode;
        private int m_selectedMoveMode;
        private int m_hoveredPoint = -1;
        private int m_selectedPoint = -1;
        private int m_hoveredLine = -1;
        private Tool m_lastTool;

        private static readonly Color s_startPointColor = new Color32 (0, 185, 165, 255);
        private static readonly Color s_endPointColor = new Color32 (185, 0, 65, 200);
        private static readonly Color s_normalColor = new Color32 (62, 95, 150, 255);
        private static readonly Color s_selectedNormalColor = new Color32 (120, 190, 255, 255);
        private static GUIStyle s_labelStyle;

    #endregion


    #region Properties

        private float pointDistanceThreshold {
            get { return m_scale * 0.1F; }
        }

        private float handleCircleSize {
            get { return 0.3F * m_scale; }
        }

        private GUIStyle labelStyle {
            get {
                if (s_labelStyle == null) {
                    s_labelStyle = new GUIStyle ();
                }

                s_labelStyle.fontSize = (int) (25 * m_scale);
                s_labelStyle.alignment = TextAnchor.MiddleCenter;
                s_labelStyle.normal.textColor = new Color (1, 1, 1, 0.2F);
                return s_labelStyle;
            }
        }

    #endregion


    #region Unity

        void OnEnable () {
            m_path = target as AIPath;

            m_points = serializedObject.FindProperty ("m_points");
            m_isPathClosed = serializedObject.FindProperty ("m_isPathClosed");

            m_lastTool = Tools.current;
            m_oldTransformPosition = m_path.transform.position;
        }


        void OnDisable () {
            Tools.current = m_lastTool;
        }


        public override void OnInspectorGUI () {
            serializedObject.Update ();

            EditorGUILayout.PropertyField (m_isPathClosed);

            EditorGUILayout.Space ();

            UnityEditor.EditorGUI.indentLevel++;
            EditorGUILayout.BeginHorizontal ();
            m_showPoints = EditorGUILayout.Foldout (m_showPoints, "Points", true);
            GUILayout.FlexibleSpace ();

            // Reverse Points
            if (GUILayout.Button ("Reverse", GUILayout.Width (70))) {
                ReversePoints ();
            }

            // Remove Point
            GUI.enabled = m_path.numPoints > 0;
            if (GUILayout.Button ("-", GUILayout.Width (25))) {
                RemovePoint (m_path.numPoints - 1);
            }

            GUI.enabled = true;

            // Add Point
            if (GUILayout.Button ("+", GUILayout.Width (25))) {
                if (m_path.numPoints >= 2) {
                    Vector3 dir = m_path[m_path.numPoints - 1] - m_path[m_path.numPoints - 2];
                    dir.Normalize ();
                    Vector3 newPosition = m_path[m_path.numPoints - 1] + dir * 2;
                    AddPoint (newPosition);
                }
                else if (m_path.numPoints == 1) {
                    AddPoint (m_path[m_path.numPoints - 1] + Vector3.forward * 2);
                }
                else {
                    AddPoint (m_path.transform.position);
                }
            }

            EditorGUILayout.EndHorizontal ();
            UnityEditor.EditorGUI.indentLevel--;

            if (m_showPoints) {
                // Groups are only for indentLevel strange behaviour.
                EditorGUILayout.BeginHorizontal ();

                EditorGUILayout.BeginVertical ();
                EditorGUILayout.LabelField ("", GUILayout.Width (10));
                EditorGUILayout.EndVertical ();

                EditorGUILayout.BeginVertical ();

                EditorGUILayout.Space ();

                for (int i = 0; i < m_points.arraySize; i++) {
                    EditorGUILayout.BeginHorizontal ();
                    EditorGUILayout.LabelField (i.ToString (), GUILayout.Width (20));
                    Vector3 oldPoint = m_points.GetArrayElementAtIndex (i).vector3Value;
                    m_points.GetArrayElementAtIndex (i).vector3Value = EditorGUILayout.Vector3Field (GUIContent.none, oldPoint);
                    if (GUILayout.Button ("X", GUILayout.Width (20))) {
                        RemovePoint (i);
                    }

                    EditorGUILayout.EndHorizontal ();
                }

                EditorGUILayout.EndVertical ();
                EditorGUILayout.EndHorizontal ();
            }

            serializedObject.ApplyModifiedProperties ();

            EditorGUILayout.Space ();
        }


        void OnSceneGUI () {
            if (IsEditMode ()) {
                PerformEvent (Event.current);
            }

            DrawPath ();

            // preventing from clicking on other objects.
            if (IsEditMode ()) {
                Tools.current = Tool.None;

                if (Event.current.type == EventType.Layout) {
                    HandleUtility.AddDefaultControl (GUIUtility.GetControlID (GetHashCode (), FocusType.Passive));
                }
            }


            DrawSceneSettings (SceneView.currentDrawingSceneView);

            MovePointsWithTransform ();
        }

#endregion


    #region Internal

        void MovePointsWithTransform () {
            if (m_path.transform.position != m_oldTransformPosition) {
                Vector3 offset = m_path.transform.position - m_oldTransformPosition;

                for (int i = 0; i < m_path.numPoints; i++) {
                    m_path.MovePoint (i, m_path[i] + offset);
                }
            }

            m_oldTransformPosition = m_path.transform.position;
        }


        /// <summary>
        /// Returns the correct color of a point.
        /// </summary>
        Color GetPointColor (int i) {
            return i == m_selectedPoint || i == m_hoveredPoint ? s_selectedNormalColor : s_normalColor;
        }


        /// <summary>
        /// Returns the correct color of a line.
        /// </summary>
        Color GetLineColor (int i) {
            return i == m_hoveredLine ? s_selectedNormalColor : s_normalColor;
        }


        /// <summary>
        /// Performs all events for the path editor.
        /// </summary>
        void PerformEvent (Event e) {
            // add point
            if (e.IsMouseDown (MouseButton.Left) && e.shift) {
                if (TryGetMousePosition (out Vector3 mousePosition)) {
                    if (m_hoveredLine != -1) {
                        AddPoint (mousePosition, m_hoveredLine + 1);
                    }
                    else {
                        AddPoint (mousePosition);
                    }

                    e.Use ();
                }
                else {
                    Debug.LogWarning ("Not looking at the ground.");
                }
            }

            // remove point
            if (e.IsMouseDown (MouseButton.Right) && e.shift) {
                if (TryGetMousePosition (out Vector3 mousePosition)) {
                    for (int i = 0; i < m_path.numPoints; i++) {
                        if (Vector3.Distance (m_path[i], mousePosition) < handleCircleSize) {
                            RemovePoint (i);
                            e.Use ();
                            break;
                        }
                    }
                }
            }

            // select point
            if (e.IsMouseDown (MouseButton.Left)) {
                if (TryGetMousePosition (out Vector3 mousePosition)) {
                    bool clickedOnPoint = false;
                    for (int i = 0; i < m_path.numPoints; i++) {
                        if (Vector3.Distance (m_path[i], mousePosition) < handleCircleSize) {
                            m_selectedPoint = i;
                            clickedOnPoint = true;
                            Repaint ();
                            break;
                        }
                    }

                    if (!clickedOnPoint) {
                        Repaint ();
                        m_selectedPoint = -1;
                    }

                    HandleUtility.Repaint ();
                }
            }

            // hover point
            if (e.IsMouseMove ()) {
                int newHoveredPoint = -1;

                if (TryGetMousePosition (out Vector3 mousePosition)) {
                    float minDstToPoint = handleCircleSize;

                    for (int i = 0; i < m_path.numPoints; i++) {
                        float dst = Vector3.Distance (m_path[i], mousePosition);

                        if (dst < minDstToPoint) {
                            minDstToPoint = dst;
                            newHoveredPoint = i;
                        }
                    }
                }

                if (newHoveredPoint != m_hoveredPoint) {
                    m_hoveredPoint = newHoveredPoint;
                    HandleUtility.Repaint ();
                }
            }

            //hover line
            if (e.IsMouseMove ()) {
                int newHoveredLine = -1;
                if (TryGetMousePosition (out Vector3 mousePosition) && m_hoveredPoint < 0) {
                    if (m_path.isPathClosed) {
                        float minDstToPoint = pointDistanceThreshold;

                        for (int i = 0; i < m_path.numPoints; i++) {
                            float dst = HandleUtility.DistancePointLine (mousePosition, m_path[i],
                                m_path[(i + 1) % m_path.numPoints]);

                            if (dst < minDstToPoint) {
                                minDstToPoint = dst;
                                newHoveredLine = i;
                            }
                        }
                    }
                    else {
                        float minDstToPoint = pointDistanceThreshold * 3;

                        for (int i = 0; i < m_path.numPoints - 1; i++) {
                            float dst = HandleUtility.DistancePointLine (mousePosition, m_path[i], m_path[i + 1]);

                            if (dst < minDstToPoint) {
                                minDstToPoint = dst;
                                newHoveredLine = i;
                            }
                        }
                    }
                }

                if (newHoveredLine != m_hoveredLine) {
                    m_hoveredLine = newHoveredLine;
                    HandleUtility.Repaint ();
                }
            }
        }


        /// <summary>
        /// Returns the mouse position in the scene. 
        /// </summary>
        bool TryGetMousePosition (out Vector3 position) {
            SceneView view = SceneView.currentDrawingSceneView;
            Vector2 screenPosition = Event.current.mousePosition;
            screenPosition.y = view.camera.pixelHeight - screenPosition.y;
            Ray ray = view.camera.ScreenPointToRay (screenPosition);
            Plane plane = new Plane (Vector3.up, Vector3.zero);

            if (plane.Raycast (ray, out float distance)) {
                position = ray.GetPoint (distance);
                return true;
            }

            position = Vector3.zero;
            return false;
        }


        bool IsEditMode () {
            return m_selectedMode == 1;
        }


        void RemovePoint (int index) {
            UnityEditor.Undo.RecordObject (m_path, "Remove point");
            m_path.RemovePoint (index);
        }


        void AddPoint (Vector3 point) {
            UnityEditor.Undo.RecordObject (m_path, "Add point");
            m_path.AddPoint (point);
        }


        void AddPoint (Vector3 point, int index) {
            UnityEditor.Undo.RecordObject (m_path, "Add point");
            m_path.AddPoint (point, index);
        }


        void MovePoint (int index, Vector3 newPosition) {
            UnityEditor.Undo.RecordObject (m_path, "Move point");
            m_path.MovePoint (index, newPosition);
        }


        void ReversePoints () {
            UnityEditor.Undo.RecordObject (m_path, "Reverse points");
            m_path.Reverse ();
        }

    #endregion


    #region GUI

        void DrawSceneSettings (SceneView view) {
            Handles.BeginGUI ();

            GUILayout.BeginArea (new Rect (view.camera.pixelWidth / 2 - 300 / 2F, 30, 300, 60));

            EditorGUILayout.BeginVertical ();

            int selectedMode = GUILayout.Toolbar (m_selectedMode, new[] {"View", "Edit"});
            if (selectedMode != m_selectedMode) {
                // view selected
                if (selectedMode == 0) {
                    Tools.current = m_lastTool;
                }

                // edit selected
                else {
                    m_lastTool = Tools.current;
                }

                m_selectedMode = selectedMode;
            }

            m_selectedMoveMode = GUILayout.Toolbar (m_selectedMoveMode, new[] {"XZ", "XYZ", "X", "Y", "Z"});

            GUILayout.BeginHorizontal ();
            GUILayout.FlexibleSpace ();
            m_scale = GUILayout.HorizontalSlider (m_scale, 0.1F, 1F, GUILayout.Width (290));
            GUILayout.FlexibleSpace ();
            GUILayout.EndHorizontal ();

            EditorGUILayout.EndVertical ();

            GUILayout.EndArea ();

            Handles.EndGUI ();
        }


        void DrawPath () {
            // Draw Lines
            if (m_path.isPathClosed) {
                for (int i = 0; i < m_path.numPoints; i++) {
                    Handles.color = GetLineColor (i);

                    DrawLine (m_path[i], m_path[(i + 1) % m_path.numPoints], m_scale);
                }
            }
            else {
                for (int i = 1; i < m_path.numPoints; i++) {
                    Handles.color = GetLineColor (i - 1);

                    Vector3 direction = (m_path[i - 1] - m_path[i]).normalized;
                    Vector3 side1 = new Vector3 (-direction.z, 0, direction.x).normalized * 0.2F * m_scale;
                    Vector3 side2 = new Vector3 (direction.z, 0, -direction.x).normalized * 0.2F * m_scale;
                    DrawLine (side1 + m_path[i - 1], side1 + m_path[i], m_scale);
                    DrawLine (side2 + m_path[i], side2 + m_path[i - 1], m_scale);
                }
            }

            // Draw Points
            for (int i = 0; i < m_path.numPoints; i++) {
                Vector3 oldPos = m_path[i];
                if (i == 0) {
                    Handles.color = s_startPointColor;
                    Handles.DrawSolidDisc (oldPos, Vector3.up, handleCircleSize * 1.2F);
                }
                else if (i == m_path.numPoints - 1) {
                    Handles.color = s_endPointColor;
                    Handles.DrawSolidDisc (oldPos, Vector3.up, handleCircleSize * 1.2F);
                }

                Handles.color = GetPointColor (i);
                Handles.DrawSolidDisc (oldPos, Vector3.up, handleCircleSize);

                if (IsEditMode ()) {
                    if (i == 0) {
                        Handles.Label (oldPos, "Start", labelStyle);
                    }
                    else if (i == m_path.numPoints - 1) {
                        Handles.Label (oldPos, "End", labelStyle);
                    }
                    else {
                        Handles.Label (oldPos, i.ToString (), labelStyle);
                    }


                    Handles.color = Color.clear;
                    Vector3 newPos = Handles.FreeMoveHandle (oldPos, Quaternion.identity, handleCircleSize, Vector3.zero, Handles.CircleHandleCap);

                    if (oldPos != newPos) {
                        switch (m_selectedMoveMode) {
                            case 0:
                                newPos.y = oldPos.y;
                                break;
                            case 1:
                                break;
                            case 2:
                                newPos.y = oldPos.y;
                                newPos.z = oldPos.z;
                                break;
                            case 3:
                                newPos.x = oldPos.x;
                                newPos.z = oldPos.z;
                                break;
                            case 4:
                                newPos.x = oldPos.x;
                                newPos.y = oldPos.y;
                                break;
                        }

                        MovePoint (i, newPos);
                    }
                }
            }
        }


        static void DrawLine (Vector3 from, Vector3 to, float scale) {
            Vector3 direction = (from - to).normalized;

            Vector3 left = new Vector3 (-direction.z, 0, direction.x).normalized * scale;
            Vector3 right = new Vector3 (direction.z, 0, -direction.x).normalized * scale;

            float arrowWidth = 0.15F;
            float arrowLength = 0.35F;
            float lineWidth = 0.02F;
            float toSpace = 0.6F;

            Vector3[] arrow = {
                (left * arrowWidth) + to + direction * (1 - arrowLength) * scale,
                right * arrowWidth + to + direction * (1 - arrowLength) * scale,
                to + direction * arrowLength * scale
            };
            Vector3[] linePoints = {
                (left * lineWidth) + from - direction * arrowLength * scale,
                (right * lineWidth) + from - direction * arrowLength * scale,
                (right * lineWidth) + to + direction * toSpace * scale,
                (left * lineWidth) + to + direction * toSpace * scale,
            };

            Handles.DrawAAConvexPolygon (linePoints);
            Handles.DrawAAConvexPolygon (arrow);
        }


        public static void DrawPath (AIPath path, float handleCircleSize = 0.3F, float scale = 1) {
            // Draw Lines
            if (path.isPathClosed) {
                for (int i = 0; i < path.numPoints; i++) {
                    Handles.color = s_normalColor;

                    DrawLine (path[i], path[(i + 1) % path.numPoints], scale);
                }
            }
            else {
                for (int i = 1; i < path.numPoints; i++) {
                    Handles.color = s_normalColor;

                    Vector3 direction = (path[i - 1] - path[i]).normalized;
                    Vector3 side1 = new Vector3 (-direction.z, 0, direction.x).normalized * 0.2F * scale;
                    Vector3 side2 = new Vector3 (direction.z, 0, -direction.x).normalized * 0.2F * scale;
                    DrawLine (side1 + path[i - 1], side1 + path[i], scale);
                    DrawLine (side2 + path[i], side2 + path[i - 1], scale);
                }
            }

            // Draw Points
            for (int i = 0; i < path.numPoints; i++) {
                Vector3 oldPos = path[i];
                if (i == 0) {
                    Handles.color = s_startPointColor;
                    Handles.DrawSolidDisc (oldPos, Vector3.up, handleCircleSize * 1.2F);
                }
                else if (i == path.numPoints - 1) {
                    Handles.color = s_endPointColor;
                    Handles.DrawSolidDisc (oldPos, Vector3.up, handleCircleSize * 1.2F);
                }

                Handles.color = s_normalColor;
                Handles.DrawSolidDisc (oldPos, Vector3.up, handleCircleSize);
            }
        }

    #endregion

    }


}
#endif